package com.iplay.service.hotel;

import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import java.util.stream.Collectors;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.mapping.PropertyReferenceException;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import com.iplay.component.naming.UUIDNamingStrategy;
import com.iplay.component.util.DelimiterUtils;
import com.iplay.dao.hotel.BanquetHallDAO;
import com.iplay.dao.hotel.FeastDAO;
import com.iplay.dao.hotel.HotelDAO;
import com.iplay.dao.hotel.rating.HotelRatingDAO;
import com.iplay.dto.hotel.AddressDTO;
import com.iplay.dto.hotel.HotelDTO;
import com.iplay.dto.hotel.SimplifiedBanquetHallDTO;
import com.iplay.dto.hotel.SimplifiedFeastDTO;
import com.iplay.dto.hotel.SimplifiedHotelAdminDTO;
import com.iplay.dto.hotel.SimplifiedHotelDTO;
import com.iplay.entity.common.EntityIdDO;
import com.iplay.entity.hotel.AddressDO;
import com.iplay.entity.hotel.BanquetHallDO;
import com.iplay.entity.hotel.FeastDO;
import com.iplay.entity.hotel.HotelDO;
import com.iplay.entity.hotel.rating.HotelRatingDO;
import com.iplay.service.exception.InvalidRequestParametersException;
import com.iplay.service.query.Query;
import com.iplay.service.query.QueryBuilder;
import com.iplay.service.query.specification.QuerySepcification;
import com.iplay.service.recommendation.RecommendationService;
import com.iplay.service.storage.StorageService;
import com.iplay.vo.common.EntityDeletionVO;
import com.iplay.vo.common.FileDeletionVO;
import com.iplay.vo.common.PostFilesVO;
import com.iplay.vo.hotel.HotelFilterVO;
import com.iplay.vo.hotel.PostBanquetHallVO;
import com.iplay.vo.hotel.PostFeastVO;
import com.iplay.vo.hotel.PostHotelVO;
import com.iplay.web.resource.ResourcesUriBuilder;

@Service
public class HotelServiceImpl implements HotelService {

	private static final Logger LOGGER = LoggerFactory.getLogger(HotelServiceImpl.class);

	@Autowired
	private StorageService storageService;

	@Autowired
	private HotelDAO hotelDAO;

	@Autowired
	private HotelRatingDAO hotelRatingDAO;

	@Autowired
	private BanquetHallDAO banquetHallDAO;

	@Autowired
	private FeastDAO feastDAO;
	
	@Autowired
	private QueryBuilder<HotelFilterVO> queryBuilder;
	
	@Autowired
	private RecommendationService recommendationService;
	
	@Override
	public List<SimplifiedHotelDTO> listHotel(int pageNumber, int pageSize) {
		PageRequest pageRequest = new PageRequest(pageNumber, pageSize, null);
		Page<HotelDO> hotelPages = hotelDAO.findAll(pageRequest);
		List<HotelDO> hotelDOs = hotelPages.getContent();
		List<SimplifiedHotelDTO> hotels = hotelDOs.stream().map(hotelDO -> hotelDOToSimplifiedHotelDTO(hotelDO))
				.collect(Collectors.toList());
		return hotels;
	}

	@Override
	public List<SimplifiedHotelDTO> listHotelsByIds(List<Integer> hotelIds) {
		List<SimplifiedHotelDTO> simplifiedHotels = new LinkedList<>();
		Iterable<HotelDO> hotels = hotelDAO.findAll(hotelIds);
		hotels.forEach(hotelDO -> simplifiedHotels.add(hotelDOToSimplifiedHotelDTO(hotelDO)));
		return simplifiedHotels;
	}

	@Override
	public List<SimplifiedHotelDTO> listHotelsByPage(Pageable pageable) {
		try {
			Page<HotelDO> hotelPages = hotelDAO.findAll(pageable);
			return hotelPages.map(hotelDO -> hotelDOToSimplifiedHotelDTO(hotelDO)).getContent();
		} catch (PropertyReferenceException e) {
			throw new InvalidRequestParametersException(e.getMessage());
		}
	}

	private SimplifiedHotelDTO hotelDOToSimplifiedHotelDTO(HotelDO hotelDO) {
		return new SimplifiedHotelDTO(hotelDO.getId(), hotelDO.getName(),
				new double[] { hotelDO.getMinimumPrice() == -1 ? 0 : hotelDO.getMinimumPrice(),
						hotelDO.getMaximumPrice() == -1 ? 0 : hotelDO.getMaximumPrice() },
				new int[] { hotelDO.getMinimumTables() == -1 ? 0 : hotelDO.getMinimumTables(),
						hotelDO.getMaximunTables() == -1 ? 0 : hotelDO.getMaximunTables() },
				hotelDO.getAddress().getDistrict(), hotelDO.getNumOfReviews(),
				hotelDO.getPicturesAsArray().length > 0 ? ResourcesUriBuilder.buildUri(hotelDO.getPicturesAsArray()[0])
						: "");
	}

	@Override
	public List<SimplifiedHotelAdminDTO> listHotel() {
		Iterable<HotelDO> hotels = hotelDAO.findAll();
		List<SimplifiedHotelAdminDTO> hotelList = new LinkedList<SimplifiedHotelAdminDTO>();
		for (HotelDO hotel : hotels) {
			hotelList.add(new SimplifiedHotelAdminDTO(hotel.getId(), hotel.getName(), hotel.getContact(),
					hotel.getTelephone(), hotel.getEmail(), hotel.getAddress().formatAsString()));
		}
		return hotelList;
	}

	@Override
	@Transactional
	public int saveHotel(PostHotelVO hotel) {
		AddressDO addressDO = new AddressDO(hotel.getCityOfAddress(), hotel.getDistrictOfAddress(),
				hotel.getStreetOfAddress());
		HotelDO hotelDO = new HotelDO(hotel.getName(), hotel.getDescription(), addressDO, hotel.getContact(),
				hotel.getTelephone(), hotel.getEmail(), "");
		boolean isCreated = true;
		if (hotel.getId() != -1) {
			isCreated = false;
			HotelDO findedHotelDO = hotelDAO.findOne(hotel.getId());
			if (findedHotelDO == null) {
				return -1;
			}
			addressDO.setId(findedHotelDO.getAddress().getId());
			hotelDO.setId(hotel.getId());
			hotelDO.setPictures(findedHotelDO.getPictures());
		}
		HotelDO savedHotel = hotelDAO.save(hotelDO);
		int hotelId = savedHotel.getId();
		if (isCreated) {
			try {
				hotelRatingDAO.save(new HotelRatingDO(hotelId, 0.0, 0, 0.0));
			} catch (DataIntegrityViolationException e) {
				LOGGER.error(e.getMessage());
			}
		}
		return hotelId;
	}

	@Override
	@Transactional
	public int addBanquetHall(PostBanquetHallVO banquetHallVO, int hotelId) {
		BanquetHallDO banquetHallDO = new BanquetHallDO();
		BeanUtils.copyProperties(banquetHallVO, banquetHallDO, "id");
		HotelDO hotel = hotelDAO.findOne(hotelId);
		if (hotel == null)
			return -1;
		updateTableRange(hotel, banquetHallDO.getMinimumTables(), banquetHallDO.getMaximumTables());
		banquetHallDO.setHotelDO(hotel);
		banquetHallDAO.save(banquetHallDO);
		return banquetHallDO.getId();
	}

	@Override
	public int addFeast(PostFeastVO feastVO, int hotelId) {
		FeastDO feastDO = new FeastDO();
		BeanUtils.copyProperties(feastVO, feastDO, "id");
		HotelDO hotel = hotelDAO.findOne(hotelId);
		if (hotel == null)
			return -1;
		updatePriceRange(hotel, feastDO.getPrice(), feastDO.getPrice());
		feastDO.setHotelDO(hotel);
		feastDAO.save(feastDO);
		return feastDO.getId();
	}

	@Override
	public HotelDTO findHotelById(int id) {
		HotelDO hotel = hotelDAO.findOne(id);
		if (hotel == null)
			return null;
		List<SimplifiedFeastDTO> feasts = hotel.getFeasts().stream()
				.map(feastDO -> new SimplifiedFeastDTO(feastDO.getId(), feastDO.getName(), feastDO.getPrice()))
				.collect(Collectors.toList());
		List<SimplifiedBanquetHallDTO> banquetHalls = hotel.getBanquetHalls().stream()
				.map(banquetHallDO -> new SimplifiedBanquetHallDTO(banquetHallDO.getId(), banquetHallDO.getName(),
						banquetHallDO.getArea(),
						new int[] { banquetHallDO.getMinimumTables(), banquetHallDO.getMaximumTables() },
						banquetHallDO.getMinimumPrice(),
						banquetHallDO.getPicturesAsArray().length > 0
								? ResourcesUriBuilder.buildUri(banquetHallDO.getPicturesAsArray()[0])
								: ""))
				.collect(Collectors.toList());
		String[] pictures = ResourcesUriBuilder.buildUris(hotel.getPicturesAsArray());
		AddressDTO address = new AddressDTO();
		BeanUtils.copyProperties(hotel.getAddress(), address);
		return new HotelDTO(hotel.getId(), hotel.getName(), hotel.getDescription(), address, hotel.getContact(),
				hotel.getTelephone(), hotel.getEmail(),
				new int[] { hotel.getMinimumTables() == -1 ? 0 : hotel.getMinimumTables(),
						hotel.getMaximunTables() == -1 ? 0 : hotel.getMaximunTables() },
				new double[] { hotel.getMinimumPrice() == -1 ? 0 : hotel.getMinimumPrice(),
						hotel.getMaximumPrice() == -1 ? 0 : hotel.getMaximumPrice() },
				hotel.getRating(), hotel.getNumOfReviews(), banquetHalls, feasts, pictures);
	}

	@Override
	public boolean deleteHotel(int id) {
		HotelDO hotel = hotelDAO.findOne(id);
		if (hotel != null) {
			hotelDAO.delete(id);
			storageService.delete(hotel.getPicturesAsArray());
			recommendationService.deleteRecommendationByHotelId(id);
		}
		return true;
	}

	@Override
	public String[] savePictures(int hotelId, PostFilesVO files) {
		HotelDO hotel = hotelDAO.findOne(hotelId);
		if (hotel == null)
			return null;
		MultipartFile[] mfs = files.getFiles();
		String[] savedFilenames = new String[mfs.length];
		for (int i = 0; i < mfs.length; i++) {
			savedFilenames[i] = storageService.store(mfs[i], UUIDNamingStrategy.generateUUID());
		}
		String pictures = DelimiterUtils.joinArray(savedFilenames, DelimiterUtils.GLOBAL_DEFAULT_DELIMITER);
		hotelDAO.addPictures(hotelId, pictures);
		return ResourcesUriBuilder.buildUris(savedFilenames);
	}

	@Override
	public boolean[] deletePictures(int hotelId, FileDeletionVO fileDeletionVO) {
		HotelDO hotel = hotelDAO.findOne(hotelId);
		if (hotel == null)
			return null;
		String pictures = hotel.getPictures();
		String[] files = fileDeletionVO.getNames();
		boolean[] rs = new boolean[files.length];
		for (int i = 0; i < files.length; i++) {
			rs[i] = storageService.delete(files[i]);
			pictures = pictures.replace(files[i] + DelimiterUtils.GLOBAL_DEFAULT_DELIMITER, "");
		}
		hotel.setPictures(pictures);
		hotelDAO.save(hotel);
		return rs;
	}

	@Override
	public boolean deleteHotels(EntityDeletionVO entityDeletionVO) {
		List<HotelDO> hotels = new LinkedList<>();
		List<String> pictures = new LinkedList<>();
		List<Integer> hotelIds = new LinkedList<>();
		hotelDAO.findAll(entityDeletionVO.getIds()).forEach(h -> {
			pictures.addAll(Arrays.asList(h.getPicturesAsArray()));
			hotels.add(h);
			hotelIds.add(h.getId());
		});
		hotelDAO.delete(hotels);
		recommendationService.deleteRecommendationByHotels(hotelIds);
		storageService.delete(pictures);
		return true;
	}

	@Override
	public void updateTableRange(HotelDO hotel, int minimumTables, int maximumTables) {
		int originalMinimumTables = hotel.getMinimumTables(), originalMaximumTables = hotel.getMaximunTables();
		if (originalMinimumTables == -1 && originalMaximumTables == -1) {
			hotel.setMinimumTables(minimumTables);
			hotel.setMaximunTables(maximumTables);
		} else {
			if (originalMinimumTables > minimumTables)
				hotel.setMinimumTables(minimumTables);
			if (originalMaximumTables < maximumTables)
				hotel.setMaximunTables(maximumTables);
		}
		if (originalMinimumTables != hotel.getMinimumTables() || originalMaximumTables != hotel.getMaximunTables()) {
			hotelDAO.updateTableRange(hotel.getId(), hotel.getMinimumTables(), hotel.getMaximunTables());
		}
	}

	@Override
	public void updatePriceRange(HotelDO hotel, double minimumPrice, double maximumPrice) {
		double originalMinimumPrice = hotel.getMinimumPrice(), originalMaximumPrice = hotel.getMaximumPrice();
		if (originalMinimumPrice == -1 && originalMaximumPrice == -1) {
			hotel.setMinimumPrice(minimumPrice);
			hotel.setMaximumPrice(maximumPrice);
		} else {
			if (originalMinimumPrice > minimumPrice)
				hotel.setMinimumPrice(minimumPrice);
			if (originalMaximumPrice < maximumPrice)
				hotel.setMaximumPrice(maximumPrice);
		}
		if (originalMinimumPrice != hotel.getMinimumPrice() || originalMaximumPrice != hotel.getMaximumPrice()) {
			hotelDAO.updatePriceRange(hotel.getId(), hotel.getMinimumPrice(), hotel.getMaximumPrice());
		}
	}

	@Override
	@Async
	@Transactional
	public void reCalculateTableRange(int hotelId) {
		HotelDO hotel = hotelDAO.findOne(hotelId);
		if (hotel == null)
			return;
		int[] tableRange = { Integer.MAX_VALUE, -1 };
		boolean[] hasTables = new boolean[] { false };
		hotel.getBanquetHalls().forEach(bh -> {
			hasTables[0] = true;
			if (tableRange[0] > bh.getMinimumTables()) {
				tableRange[0] = bh.getMinimumTables();
			}
			if (tableRange[1] < bh.getMaximumTables()) {
				tableRange[1] = bh.getMaximumTables();
			}
		});
		if (!hasTables[0])
			tableRange[0] = -1;
		if (hotel.getMinimumTables() != tableRange[0] || hotel.getMaximunTables() != tableRange[1]) {
			hotelDAO.updateTableRange(hotelId, tableRange[0], tableRange[1]);
		}
	}

	@Override
	@Async
	@Transactional
	public void reClaculatePriceRange(int hotelId) {
		HotelDO hotel = hotelDAO.findOne(hotelId);
		if (hotel == null)
			return;
		double[] priceRange = { Double.MAX_VALUE, -1 };
		boolean[] hasPrice = { false };
		hotel.getFeasts().forEach(f -> {
			hasPrice[0] = true;
			if (priceRange[0] > f.getPrice()) {
				priceRange[0] = f.getPrice();
			}
			if (priceRange[1] < f.getPrice()) {
				priceRange[1] = f.getPrice();
			}
		});
		if (!hasPrice[0])
			priceRange[0] = -1;
		if (hotel.getMinimumPrice() != priceRange[0] || hotel.getMaximumPrice() != priceRange[1]) {
			hotelDAO.updatePriceRange(hotelId, priceRange[0], priceRange[1]);
		}
	}

	@Override
	public List<Integer> findHotelByName(String hotel) {
		List<EntityIdDO> entities = hotelDAO.findByName(hotel);
		return entities.stream().map(e -> e.getId()).collect(Collectors.toList());
	}

	@Override
	public List<SimplifiedHotelDTO> listHotelsByFilter(HotelFilterVO filter, Pageable pageable) {
		Query query =queryBuilder.create(filter);
		QuerySepcification<HotelDO> spec = new QuerySepcification<>(query);
		Page<HotelDO> hotels = hotelDAO.findAll(spec, pageable);
		return hotels.map(hotelDO -> hotelDOToSimplifiedHotelDTO(hotelDO)).getContent();
	}
}
